﻿Imports System.Net
Imports System.IO
Imports System.Text

' Original code by "doskabouter"
' Translated to VB.NET by Xavier Flix

Public Enum AMF0Type
    NumberMarker = &H0
    BooleanMarker = &H1
    StringMarker = &H2
    ObjectMarker = &H3
    MovieClipMarker = &H4
    NullMarker = &H5
    UndefinedMarker = &H6
    ReferenceMarker = &H7
    EcmaArrayMarker = &H8
    ObjectEndMarker = &H9
    StrictArrayMarker = &HA
    DateMarker = &HB
    LongStringMMarker = &HC
    UnsupportedMarker = &HD
    RecordsetMarker = &HE
    XmlDocumentMarker = &HF
    TypedObjectMarker = &H10
    AvmplusObjectMarker = &H11
End Enum

Public Enum AMF3Type
    UndefinedMarker = &H0
    NullMarker = &H1
    FalseMarker = &H2
    TrueMarker = &H3
    IntegerMarker = &H4
    DoubleMarker = &H5
    StringMarker = &H6
    XmlDocMarker = &H7
    DateMarker = &H8
    ArrayMarker = &H9
    ObjectMarker = &HA
    XmlMarker = &HB
    ByteArrayMarker = &HC
End Enum

Public Class AMFObject
    Public Name As String
    Public Properties As Dictionary(Of String, Object)

    Private keys As New List(Of String)()

    Public Sub New(objName As String)
        Name = objName
        Properties = New Dictionary(Of String, Object)()
    End Sub

    Public Sub New(objName As String, properties As Dictionary(Of String, Object))
        Name = objName
        Me.Properties = properties
    End Sub

    Public Function Clone() As AMFObject
        Dim res As AMFObject = New AMFObject(Name)
        res.Properties = New Dictionary(Of String, Object)()
        For i = 0 To keys.Count - 1
            res.keys.Add(keys(i))
        Next
        Return res
    End Function

    Public Sub Add(key As String, value As Object)
        Properties.Add(key, value)
    End Sub

    Public Function GetObject(key As String) As AMFObject
        If Properties.ContainsKey(key) Then Return CType(Properties(key), AMFObject)
        Return Nothing
    End Function

    Public Function GetArray(key As String) As AMFArray
        If Properties.ContainsKey(key) Then Return CType(Properties(key), AMFArray)
        Return Nothing
    End Function

    Public Function GetStringProperty(key As String) As String
        If Properties.ContainsKey(key) Then Return CType(Properties(key), String)
        Return ""
    End Function

    Public Function GetIntProperty(key As String) As Integer
        If Properties.ContainsKey(key) Then
            Dim obj = Properties(key)
            If TypeOf obj Is Integer Then Return CType(obj, Integer)
        End If
        Return -1
    End Function

    Public Function GetDoubleProperty(key As String) As Double
        If Properties.ContainsKey(key) Then
            Dim obj = Properties(key)
            If TypeOf obj Is Double Then Return CType(obj, Double)
        End If
        Return Double.NaN
    End Function

    Public Sub AddKey(key As String)
        keys.Add(key)
    End Sub

    Public ReadOnly Property KeyCount() As Integer
        Get
            Return keys.Count
        End Get
    End Property

    Public ReadOnly Property Key(i As Integer) As String
        Get
            Return keys(i)
        End Get
    End Property

    Public Shared Function GetResponse(url As String, postData() As Byte)
        Log.Debug("get webdata from {0}", url)

        Dim request As HttpWebRequest = CType(WebRequest.Create(url), HttpWebRequest)
        If request Is Nothing Then Return Nothing
        request.Method = "POST"
        request.ContentType = "application/x-amf"
        request.UserAgent = OnlineVideoSettings.Instance.UserAgent
        request.Timeout = 15000
        request.ContentLength = postData.Length
        request.ProtocolVersion = HttpVersion.Version10
        request.Headers.Add(HttpRequestHeader.AcceptEncoding, "gzip,deflate")

        Dim requestStream As Stream = request.GetRequestStream()
        requestStream.Write(postData, 0, postData.Length)
        requestStream.Close()
        Using response As HttpWebResponse = CType(request.GetResponse(), HttpWebResponse)
            Dim responseStream As Stream
            If response.ContentEncoding.ToArray().Contains("gzip") Then
                responseStream = New System.IO.Compression.GZipStream(response.GetResponseStream(), System.IO.Compression.CompressionMode.Decompress)
            ElseIf response.ContentEncoding.ToLower().Contains("deflate") Then
                responseStream = New System.IO.Compression.DeflateStream(response.GetResponseStream(), System.IO.Compression.CompressionMode.Decompress)
            Else
                responseStream = response.GetResponseStream()
            End If

            Dim des As AMFDeserializer = New AMFDeserializer(responseStream)
            Dim obj As AMFObject = des.Deserialize()
            Return obj
        End Using
    End Function
End Class

Public Class AMFArray
    Private objs As List(Of Object)
    Private strs As Dictionary(Of String, Object)

    Public Sub New()
        objs = New List(Of Object)()
    End Sub

    Public Sub New(objs As List(Of Object))
        Me.objs = objs
    End Sub

    Public Sub New(strs As Dictionary(Of String, Object))
        Me.strs = strs
    End Sub

    Public Sub Add(obj As AMFObject)
        objs.Add(obj)
    End Sub

    Public Function GetObject(index As Integer) As AMFObject
        Return CType(objs(index), AMFObject)
    End Function

    Public Function GetObject(key As String) As AMFObject
        If strs.ContainsKey(key) Then Return CType(strs(key), AMFObject)
        Return Nothing
    End Function

    Public ReadOnly Property Count As Integer
        Get
            Return objs.Count
        End Get
    End Property
End Class

Public Class AMFDeserializer
    Private Class AmfEndOfObject
    End Class

    Private reader As BinaryReader
    Private classDefinitions As New List(Of AMFObject)()
    Private strings As New List(Of String)()
    Private isInAMF3 As Boolean = False

    Public Sub New(stream As Stream)
        reader = New BinaryReader(stream)
    End Sub

    Public Function Deserialize() As AMFObject
        classDefinitions.Clear()
        strings.Clear()

        Dim version As Short = ReadShort() ' &h03
        If version = 0 Then Return DeserializeVersion0()
        Dim i As Integer = ReadShort()
        i = ReadShort()
        Dim s As String = ReadString()
        i = ReadShort()
        i = ReadShort()
        i = ReadShort()
        i = reader.ReadByte() ' &h11
        If i = CType(AMF0Type.AvmplusObjectMarker, Byte) Then isInAMF3 = True

        Dim res As Object = ReadParamValue()
        Return CType(res, AMFObject)
    End Function

    Private Function DeserializeVersion0() As AMFObject
        Dim i As Integer = ReadShort()
        i = ReadShort()
        Dim s As String = ReadString()
        s = ReadString()
        i = ReadShort()
        i = ReadShort()
        Dim res As Object = ReadParamValue()
        Return CType(res, AMFObject)
    End Function

    Private Function ReadUInt29() As Integer
        Dim result As Integer = 0
        Dim n As Integer = 0
        Dim cont As Boolean
        Do
            Dim b As Byte = reader.ReadByte()
            n += 1
            If n = 4 Then
                result = result << 8
            Else
                result = result << 7
            End If
            cont = n < 4 AndAlso b >= &H80
            If cont Then b = b And &H7F
            result = result Or b
        Loop While cont

        Return result
    End Function

    Private Function ReadShort() As Short
        Dim bytes(2 - 1) As Byte
        reader.Read(bytes, 0, 2)
        Array.Reverse(bytes)
        Return BitConverter.ToInt16(bytes, 0)
    End Function

    Private Function ReadInt32() As Integer
        Dim bytes() As Byte = reader.ReadBytes(4)
        Array.Reverse(bytes)
        Return BitConverter.ToInt32(bytes, 0)
    End Function

    Private Function ReadDouble() As Double
        Dim bytes() As Byte = reader.ReadBytes(8)
        Array.Reverse(bytes)
        Return BitConverter.ToInt32(bytes, 0)
    End Function

    Private Function ReadBoolean() As Boolean
        Dim b As Byte = reader.ReadByte()
        Return b <> 0
    End Function

    Private Function ReadString() As String
        Dim len As Short = ReadShort()
        Dim bytes(len - 1) As Byte
        reader.Read(bytes, 0, len)
        Return Encoding.UTF8.GetString(bytes)
    End Function

    Private Function ReadAmf3Date() As DateTime
        reader.ReadByte()
        Dim seconds As Double = ReadDouble() / 1000
        Dim origin As DateTime = New DateTime(1970, 1, 1, 0, 0, 0, 0)
        Dim res As DateTime = origin.AddSeconds(seconds)
        Return res
    End Function

    Private Function ReadAmf0Date() As DateTime
        Dim seconds = ReadDouble() / 1000
        Dim origin = New DateTime(1970, 1, 1, 0, 0, 0, 0)
        Dim res = origin.AddSeconds(seconds)
        Dim dummy = ReadShort()
        Return res
    End Function

    Private Function ReadParamName() As String
        Dim l As Integer = ReadUInt29()
        Dim referenced As Boolean = ((l And 1) = 0)
        l = (l >> 1)
        If referenced Then Return strings(l) ' "Referenced string nr. " + l.ToString() + "(=" + strings[l] + ")";
        If l = 0 Then Return String.Empty
        Dim bytes(l - 1) As Byte
        reader.Read(bytes, 0, l)
        Dim s As String = Encoding.UTF8.GetString(bytes)
        strings.Add(s)
        Return s
    End Function

    Private Function ReadAmf3Array() As AMFArray
        Dim v = ReadUInt29()
        Dim key As String = ReadParamName()
        Dim result As AMFArray

        If key = String.Empty Then
            Dim objs As New List(Of Object)()
            result = New AMFArray(objs)
            For i = 0 To (v >> 1) - 1
                objs.Add(ReadParamValue())
            Next
        Else
            Dim strs As New Dictionary(Of String, Object)()
            result = New AMFArray(strs)
            While key <> String.Empty
                strs.Add(key, ReadParamValue())
                key = ReadParamName()
            End While
        End If

        Return result
    End Function

    Private Function ReadAmf3Object() As AMFObject
        Dim result As AMFObject
        Dim v = ReadUInt29()
        If (v And 3) = 1 Then
            result = classDefinitions(v >> 2).Clone()
        Else
            Dim nkeys As Integer = v >> 4
            Dim name As String = ReadParamName()
            result = New AMFObject(name)

            For i = 0 To nkeys - 1
                result.AddKey(ReadParamName())
            Next

            classDefinitions.Add(result)
        End If

        For i = 0 To result.KeyCount - 1
            Dim obj As Object = ReadParamValue()
            'Debug.WriteLine(result.Key(i) + "=" + If(obj Is Nothing, "null", obj.ToString()))
            result.Properties.Add(result.Key(i), obj)
        Next

        Return result
    End Function

    Private Function ReadAmf0AnonymousObject() As AMFObject
        Dim result As AMFObject = New AMFObject(String.Empty)
        Dim endFound = False

        Do
            Dim key = ReadString()
            Dim obj As Object = ReadParamValue()
            If TypeOf obj Is AmfEndOfObject Then
                endFound = True
            Else
                result.Add(key, obj)
            End If
        Loop Until endFound
        classDefinitions.Add(result)

        Return result
    End Function

    Private Function ReadAmf0Object() As AMFObject
        Dim name = ReadString()
        ' TODO: check if exists
        Dim result As AMFObject = ReadAmf0AnonymousObject()
        result.Name = name
        Return result
    End Function

    Private Function ReadAmf0StrictArray() As AMFArray
        Dim objs As New List(Of Object)()
        Dim result = New AMFArray(objs)

        Dim l = ReadInt32()
        For i = 0 To l - 1
            objs.Add(ReadParamValue())
        Next
        Return result
    End Function

    Private Function ReadParamValue() As Object
        If isInAMF3 Then
            Dim typ As AMF3Type = CType(reader.ReadByte(), AMF3Type)
            Select Case typ
                Case AMF3Type.NullMarker : Return Nothing
                Case AMF3Type.ArrayMarker : Return ReadAmf3Array()
                Case AMF3Type.StringMarker : Return ReadParamName()
                Case AMF3Type.DoubleMarker : Return ReadDouble()
                Case AMF3Type.ObjectMarker : Return ReadAmf3Object()
                Case AMF3Type.FalseMarker : Return False
                Case AMF3Type.TrueMarker : Return True
                Case AMF3Type.IntegerMarker : Return ReadUInt29()
                Case AMF3Type.DateMarker : Return ReadAmf3Date()
                Case Else : Throw New NotImplementedException()
            End Select
        Else
            Dim typ As AMF0Type = CType(reader.ReadByte(), AMF0Type)
            Select Case typ
                Case AMF0Type.NullMarker : Return Nothing
                Case AMF0Type.StrictArrayMarker : Return ReadAmf0StrictArray()
                Case AMF0Type.StringMarker : Return ReadString()
                Case AMF0Type.NumberMarker : Return ReadDouble()
                Case AMF0Type.TypedObjectMarker : Return ReadAmf0Object()
                Case AMF0Type.ObjectEndMarker : Return New AmfEndOfObject()
                Case AMF0Type.ObjectMarker : Return ReadAmf0AnonymousObject()
                Case AMF0Type.BooleanMarker : Return ReadBoolean()
                Case AMF0Type.DateMarker : Return ReadAmf0Date()
                Case Else : Throw New NotImplementedException()
            End Select
        End If
    End Function
End Class

Public Class AMFSerializer
    Private output As List(Of Byte)

    Public Sub New()
        output = New List(Of Byte)()
    End Sub

    Public Function Serialize(obj As AMFObject, hash As String) As Byte()
        output.Clear()

        OutShort(3) ' version
        OutShort(0) ' headercount
        OutShort(1) ' responsecount
        OutString("com.brightcove.experience.ExperienceRuntimeFacade.getDataForExperience")
        OutString("/1")
        OutShort(0) ' ??

        Dim lengthpos = output.Count
        OutShort(&HA00) ' array
        OutShort(0) ' ??
        OutShort(&H202) ' ??
        OutString(hash)

        output.Add(CType(AMF0Type.AvmplusObjectMarker, Byte)) ' switch to AMF3?
        OutParamValue(obj)
        OutShort(CType((output.Count - lengthpos), Short), lengthpos)

        Return output.ToArray()
    End Function

    Public Function Serialize2(target As String, values() As Object) As Byte()
        output.Clear()

        OutShort(3) ' version
        OutShort(0) ' headercount
        OutShort(1) ' responsecount
        OutString(target)
        OutString("/1")
        OutShort(0) ' ??

        Dim lengthpos = output.Count
        output.Add(CType(AMF0Type.StrictArrayMarker, Byte))
        OutInt32(values.Length)

        For Each obj In values
            If TypeOf obj Is String Then
                output.Add(CType(AMF0Type.StringMarker, Byte))
                OutString(CType(obj, String))
            ElseIf TypeOf obj Is Double Then
                output.Add(CType(AMF0Type.NumberMarker, Byte))
                Dim bytes() As Byte
                If Double.IsNaN(CType(obj, Double)) Then
                    bytes = New Byte() {&H7F, &HFF, &HFF, &HFF, &HE0, 0, 0, 0}
                Else
                    bytes = BitConverter.GetBytes(CType(obj, Double))
                    Array.Reverse(bytes)
                End If
                output.AddRange(bytes)
            ElseIf obj Is Nothing Then
                output.Add(CType(AMF0Type.NullMarker, Byte))
            Else
                Throw New NotImplementedException()
            End If
        Next
        OutShort(CType((output.Count - lengthpos), Short), lengthpos)

        Return output.ToArray()
    End Function

    Private Sub OutInt32(value As Int32, Optional atpos As Integer = -1)
        Dim bytes() = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(value))
        If atpos = -1 Then
            output.AddRange(bytes)
        Else
            output.InsertRange(atpos, bytes)
        End If
    End Sub

    Private Sub OutShort(value As Short, Optional atpos As Integer = -1)
        Dim bytes() = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(value))
        If atpos = -1 Then
            output.AddRange(bytes)
        Else
            output.InsertRange(atpos, bytes)
        End If
    End Sub

    Private Sub OutString(value As String)
        OutShort(CType(value.Length, Short))
        output.AddRange(Encoding.UTF8.GetBytes(value))
    End Sub

    Private Sub OutParamValue(obj As Object)
        output.Add(CType(AMF3Type.ObjectMarker, Byte))
        Dim v As Integer = (obj.Properties.Count << 4) Or 3
        OutUInt29(v)
        OutParamName(obj.Name)
        OutDict(obj.Properties)
    End Sub

    Private Sub OutUInt29(paramValue As Integer)
        If paramValue < 0 OrElse paramValue >= &H40000000 Then Throw New ArgumentOutOfRangeException()
        If paramValue < &H80 Then
            output.Add(CType(paramValue, Byte))
        Else
            If paramValue < &H200000 Then
                output.Add(CType((paramValue >> 14) Or &H80, Byte))
                output.Add(CType((paramValue >> 7) And &H7F Or &H80, Byte))
                output.Add(CType(paramValue And &H7F, Byte))
            Else
                output.Add(CType((paramValue >> 22) Or &H80, Byte))
                output.Add(CType((paramValue >> 15) And &H7F Or &H80, Byte))
                output.Add(CType(paramValue >> 8, Byte))
                output.Add(CType(paramValue And &H7F, Byte))
            End If
        End If
    End Sub

    Private Sub OutParamValue(obj As AMFArray)
        output.Add(CType(AMF3Type.ArrayMarker, Byte))
        Dim v = obj.Count << 1 Or 1
        OutUInt29(v)
        output.Add(&H1)
        OutParamValue(obj.GetObject(0))
    End Sub

    Private Sub OutParamName(paramName As String)
        Dim v = paramName.Length << 1 Or 1
        OutUInt29(v)
        output.AddRange(Encoding.UTF8.GetBytes(paramName))
    End Sub

    Private Sub OutParamValue(paramValue As String)
        output.Add(CType(AMF3Type.StringMarker, Byte))
        OutParamName(paramValue)
    End Sub

    Private Sub OutParamValue(paramValue As Double)
        output.Add(CType(AMF3Type.DoubleMarker, Byte))
        Dim bytes() As Byte
        If Double.IsNaN(paramValue) Then
            bytes = New Byte() {&H7F, &HFF, &HFF, &HFF, &HE0, 0, 0, 0}
        Else
            bytes = BitConverter.GetBytes(paramValue)
            Array.Reverse(bytes)
        End If
        output.AddRange(bytes)
    End Sub

    Private Sub OutParamValue(paramValue As Integer)
        output.Add(CType(AMF3Type.IntegerMarker, Byte))
        OutUInt29(paramValue)
    End Sub

    Private Sub OutDict(properties As Dictionary(Of String, Object))
        For Each kv In properties
            OutParamName(kv.Key)
        Next

        For Each kv In properties
            If TypeOf kv.Value Is String Then
                OutParamValue(CType(kv.Value, String))
            ElseIf TypeOf kv.Value Is Double Then
                OutParamValue(CType(kv.Value, Double))
            ElseIf TypeOf kv.Value Is AMFArray Then
                OutParamValue(CType(kv.Value, AMFArray))
            ElseIf kv.Value Is Nothing Then
                output.Add(CType(AMF3Type.NullMarker, Byte))
            ElseIf TypeOf kv.Value Is Integer Then
                OutParamValue(CType(kv.Value, Integer))
            Else
                Throw New NotImplementedException()
            End If
        Next
    End Sub
End Class
